<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>5.3.2-如何代理 Object</title>
</head>

<body>
    <div id="container"></div>
</body>
<script>
    /**
     * 对于 in 操作符，应该如何拦截呢？
     * - 在 ECMA-262 规范的13.10.1 节中，明确定义了 in 操作符的运行时逻辑
     * -- 第 6 步，in 操作符的运算结果是通过调用一个叫作 HasProperty 的抽象方法得到的
     * - 关于 HasProperty 抽象方法，可以在ECMA-262 规范的 7.3.11 节中找到
     * -- 第 3 步， HasProperty 抽象方法的返回值是通过调用对象的内部方法 [[HasProperty]] 得到的
     * -  [[HasProperty]] 内部方法它对应的拦截函数名叫 has，
     *     因此我们可以通过 has 拦截函数实现对 in 操作符的代理
     * */
    const bucket = new WeakMap()

    // 在 get 拦截函数内调用 track 函数追踪变化
    function track(target, key) {
        // 没有 activeEffect，直接 return
        if (!activeEffect) return
        let depsMap = bucket.get(target)
        if (!depsMap) {
            bucket.set(target, (depsMap = new Map()))
        }
        let deps = depsMap.get(key)
        if (!deps) {
            depsMap.set(key, (deps = new Set()))
        }
        // 把当前激活的副作用函数添加到依赖集合 deps 中
        deps.add(activeEffect)
        // deps 就是一个与当前副作用函数存在联系的依赖集合
        // 将其添加到 activeEffect.deps 数组中
        activeEffect.deps.push(deps) 
    }
    // 在 set 拦截函数内调用 trigger 函数触发变化
    function trigger(target, key) {
        const depsMap = bucket.get(target)
        if (!depsMap) return
        const effects = depsMap.get(key)
        const effectsToRun = new Set()
        effects && effects.forEach(effectFn => {
            // 如果 trigger0 触发执行的副作用函数与当前正在执行的副作用函数相同，则不触发执行
            if (effectFn !== activeEffect) {  
                effectsToRun.add(effectFn)
            }
        })
        effectsToRun.forEach(effectFn => {
            // 如果一个副作用函数存在调度器，则调用该调度器，并将副作用函数作为参数传递
            if (effectFn.options.scheduler) {  
                effectFn.options.scheduler(effectFn)  
            } else {
                // 否则直接执行副作用函数（之前的默认行为）
                effectFn() 
            }
        })
    }

    // 用一个全局变量存储被注册的副作用函数
    let activeEffect
    // effect 栈
    // 个人理解，始终都会留着一个是最先执行的
    const effectStack = []  
    function effect(fn, options = {}) {
        const effectFn = () => {
            cleanup(effectFn)
            // 当调用 effect 注册副作用函数时，将副作用函数赋值给 activeEffect
            activeEffect = effectFn
            // 在调用副作用函数之前将当前副作用函数压栈
            effectStack.push(effectFn)
            // 将 fn 的执行结果存储到 res 中
            const res = fn()  
            // 在当前副作用函数执行完毕后，将当前副作用函数弹出栈，并把 activeEffect 还原为之前的值
            effectStack.pop()
            activeEffect = effectStack[effectStack.length - 1]
            return res  
        }
        // 将 options 挂载到 effectFn 上
        effectFn.options = options 
        // activeEffect.deps 用来存储所有与该副作用函数相关的依赖集合
        effectFn.deps = []
        // 只有非 lazy 的时候，才执行
        if (!options.lazy) {  
            // 执行副作用函数
            effectFn()
        }
        // 将副作用函数作为返回值返回
        return effectFn 
    }

    function cleanup(effectFn) {
        // 遍历 effectFn.deps 数组
        for (let i = 0; i < effectFn.deps.length; i++) {
            // deps 是依赖集合
            const deps = effectFn.deps[i]
            // 将 effectFn 从依赖集合中移除
            deps.delete(effectFn)
        }
        // 最后需要重置 effectFn.deps 数组
        effectFn.deps.length = 0
    }

    const obj = { foo: 1 }

    const p = new Proxy(obj, {
        get(target, key, receiver) {
            // 建立联系
            track(target, key)
            // 返回属性值
            return Reflect.get(target, key, receiver)
        },
        // 拦截 in 操作符
        has(target, key) { // 新增 has 拦截函数
            // 建立联系
            track(target, key)
            // 返回属性值
            return Reflect.has(target, key)
        },

        // 拦截设置操作
        set(target, key, newVal) {
            // 设置属性值
            target[key] = newVal
            // 把副作用函数从桶里取出并执行
            trigger(target, key)
        }
    })

    effect(() => {
        console.log('foo in p ', 'foo' in p)
    })

    setTimeout(() => {
        p.foo = 2
    }, 2000)

</script>

</html>